/*
 * Copyright 2007-2008 Andrew O'Malley
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package ws.quokka.plugin.scala;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.input.InputRequest;
import org.apache.tools.ant.taskdefs.Java;
import org.apache.tools.ant.taskdefs.Javac;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.resources.FileResource;

import ws.quokka.core.bootstrap_util.PropertiesUtil;
import ws.quokka.core.plugin_spi.support.Setter;
import ws.quokka.core.plugin_spi.support.TypedProperties;
import ws.quokka.core.util.InterruptableInputHandler;

import ws.quokka.plugin.lifecycle.LifeCycleImpl;

import java.io.File;
import java.io.IOException;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Vector;


/**
 * ScalaPlugin provides support for the Scala programming language
 */
public class ScalaPlugin extends LifeCycleImpl {
    //~ Static fields/initializers -------------------------------------------------------------------------------------

    private static final String FSC = "scala.tools.nsc.CompileClient";
    private static final String BOOTSTRAP_CLASS_PATH = "bootstrapClassPath";
    private static final String COMPILER_CLASS_PATH = "compilerClassPath";

    //~ Methods --------------------------------------------------------------------------------------------------------

    public void init() {
        Map state = getState();
        Path bootstrapPath = getResources().getPathGroupAsPath("bootstrap");
        state.put(BOOTSTRAP_CLASS_PATH, bootstrapPath);
        state.put(COMPILER_CLASS_PATH, getResources().getPathGroupAsPath("compiler"));

        // Add the scala library to the test path so users do not need to define it
        getAdditionalTestPath().append(bootstrapPath);
    }

    private Map getState() {
        return (Map)getResources().getPluginState().get(getClass().getName(), new HashMap());
    }

    private Path getAdditionalTestPath() {
        return (Path)getResources().getPluginState().get("q.lifecycle.additionTestPath", new Path(getProject()));
    }

    public void compile() {
        doCompile(new Path(getProject()), getLifeCycle().getState().getGeneratedSourcesPath());
    }

    public void testCompile() {
        Path compilePath = new Path(getProject(), getLifeCycle().getComplieOutput().getPath()); // Add the compile dir to the path
        doCompile(compilePath, getLifeCycle().getState().getGeneratedTestSourcesPath());
    }

    public void cc() {
        getProject().log("Press ENTER to start compilation. Or enter a combination of the commands below:");
        log().info(" f - force compilation of all files");
        log().info(" m - turn on manual compilation");
        log().info(" a - turn on automatic compilation");
        log().info(" s - switch compilers (use fsc if currently scalac and vice versa)");
        log().info(" r - restart fsc compile server");
        log().info(" x - exit");
        log().info("e.g. 'fr' would restart the compile server and force recompilation of all files");

        String target = properties().getString("target");
        long interval = properties().getLong("intervalSeconds");
        long originalInterval = interval;
        log().info((interval == -1) ? "Compilation is set to manual"
                                    : (
                "Compilation will occur automatically, pausing " + interval + " seconds between compilations"
            ));

        boolean useFsc = properties().getBoolean("useFsc");
        boolean first = true;

        while (true) {
            Map originalProperties = new HashMap();

            try {
                String command = first ? "" : getCommand(interval);
                first = false;

                if (isEnabled(command, 'x')) {
                    break;
                }

                if (isEnabled(command, 'r')) {
                    fscShutdown();
                }

                if (isEnabled(command, 'a')) {
                    interval = originalInterval;
                }

                if (isEnabled(command, 'm')) {
                    interval = -1;

                    continue;
                }

                if (isEnabled(command, 'f')) {
                    override(originalProperties, "q.scala.main.force", "true");
                    override(originalProperties, "q.scala.test.force", "true");
                }

                String fscValue = Boolean.toString(isEnabled(command, 's') ? (!useFsc) : useFsc);
                override(originalProperties, "q.scala.main.useFsc", fscValue);
                override(originalProperties, "q.scala.test.useFsc", fscValue);

                Vector targets = new Vector();
                targets.add(target);

                try {
                    getProject().executeTargets(targets);
                    log().info("BUILD SUCCESSFUL");
                } catch (Exception e) {
                    log().error("BUILD FAILED");
                    log().error(e.getMessage());
                }
            } finally {
                restore(originalProperties);
            }
        }
    }

    private boolean isEnabled(String commands, char command) {
        return commands.indexOf(command) != -1;
    }

    private String getCommand(long interval) {
        InputRequest request = new InputRequest("Enter command:");
        final InterruptableInputHandler inputHandler = new InterruptableInputHandler(getProject().getDefaultInputStream());
        Thread timer = null;

        if (interval != -1) {
            timer = startTimer(interval, inputHandler);
        }

        try {
            inputHandler.handleInput(request);

            if (timer != null) {
                timer.interrupt(); // Timer no longer required
            }
        } catch (BuildException e) {
            if (e.getCause() instanceof InterruptedException) {
                // Fall through ... timer expired
            } else {
                throw e;
            }
        }

        return (request.getInput() == null) ? "" : request.getInput();
    }

    private void override(Map originalProperties, String name, String value) {
        String original = getProject().getProperty(name);
        originalProperties.put(name, null);

        if (original != null) {
            PropertiesUtil.clearProperty(getProject(), name);
        }

        getProject().setProperty(name, value);
    }

    private void restore(Map originalProperties) {
        for (Iterator i = originalProperties.entrySet().iterator(); i.hasNext();) {
            Map.Entry entry = (Map.Entry)i.next();
            String name = (String)entry.getKey();
            PropertiesUtil.clearProperty(getProject(), name);

            String value = (String)entry.getValue();

            if (value != null) {
                getProject().setProperty(name, value);
            }
        }
    }

    private Thread startTimer(final long interval, final InterruptableInputHandler inputHandler) {
        Thread thread = new Thread() {
                public void run() {
                    try {
                        Thread.sleep(interval * 1000);
                    } catch (InterruptedException e) {
                        return; // Means user entered command before expiry
                    }

                    inputHandler.interrupt();
                }
            };

        thread.setDaemon(true);
        thread.start();

        return thread;
    }

    public void doCompile(Path additonalPath, Path generatedSources) {
        boolean useFsc = properties().getBoolean("useFsc", false);

        if (useFsc) {
            // Start compile server asap, to give it a chance to initialise
            CompileServerHelper helper = new CompileServerHelper(this);

            if (helper.findCompileServerPort() == -1) {
                helper.startCompileServer();
            }
        }

        Path sourcePath = getSourcePath();
        sourcePath.append(generatedSources);

        if (sourcePath.size() == 0) {
            log().warn("No sources have been found - skipping compilation.");

            return;
        }

        File destDir = properties().getFile("destDir");
        utils().mkdir(destDir);

        boolean compileJava = properties().getBoolean("compileJava", true);
        Files sourceFiles = getSourceFiles(sourcePath, properties().getBoolean("force", false));

        compileScala(additonalPath, sourcePath, sourceFiles, destDir, compileJava, useFsc, true);

        if (compileJava && (sourceFiles.getJava().size() != 0)) {
            compileJava(sourcePath, sourceFiles, destDir);
            compileScala(additonalPath, sourcePath, sourceFiles, destDir, false, useFsc, false);
        }

        setCompileTimestamp();
    }

    private void setCompileTimestamp() {
        File file = getTimestampFile();
        utils().mkdir(file.getParentFile());

        if (file.exists()) {
            file.setLastModified(System.currentTimeMillis());
        } else {
            try {
                file.createNewFile();
            } catch (IOException e) {
                throw new BuildException(e);
            }
        }
    }

    private File getTimestampFile() {
        return new File(new File(getTargetDir(), "temp"), getShortTargetName() + "-timestamp");
    }

    private void compileJava(Path sourcePath, Files sourceFiles, File destDir) {
        if (sourceFiles.getJava().size() == 0) {
            log().verbose("Skipping java compilation as files are up to date");

            return;
        }

        utils().mkdir(destDir);

        Javac javac = (Javac)getProject().createTask("javac");
        javac.setClasspath(getResources().getPathGroupAsPath("compile"));
        javac.setIncludeantruntime(false);
        javac.setDestdir(destDir);
        javac.setSrcdir(sourcePath);

        TypedProperties properties = properties().sub("javac.");
        Setter setter = new Setter(properties);
        setter.set(javac,
            new String[] {
                "bootClassPath", "bootClassPathRef", "extDirs", "encoding", "noWarn", "debug", "debugLevel", "optimize",
                "deprecation", "target", "verbose", "depend", "fork", "executable", "memoryInitialSize", "failOnError",
                "source", "compiler", "listFiles", "tempDir"
            });

        // Manually check forked status, otherwise Javac prints out an annoying warning
        String memoryMaximumSize = properties.getString("maxMemory", null);

        if ((memoryMaximumSize != null) && javac.isForkedJavac()) {
            javac.setMemoryMaximumSize(memoryMaximumSize);
        }

        String compilerArgs = properties.getString("compilerArgs", null);

        if (compilerArgs != null) {
            Javac.ImplementationSpecificArgument argument = javac.createCompilerArg();
            argument.setLine(compilerArgs);
        }

        javac.perform();
    }

    private void compileScala(Path additionalPath, Path sourcePath, Files sourceFiles, File destDir,
        boolean compileJava, boolean useFsc, boolean waitForFsc) {
        if (sourceFiles.getScala().size() == 0) {
            log().verbose("Skipping scala compilation as files are up to date");

            return;
        }

        log().info("Compiling " + sourceFiles.toString(compileJava) + " with " + (useFsc ? "fsc" : "scalac"));

        Java scalac = createScalaCommand("scalac");
        scalac.setInputString("dummy"); // Blocks stdin if allowed to fork against it

        // Set up classpath
        Path classpath = new Path(getProject());
        classpath.append(additionalPath);
        classpath.append(getResources().getPathGroupAsPath("compile"));
        scalac.createArg().setValue("-classpath");
        scalac.createArg().setPath(classpath);

        // Set up sourcepath
        scalac.createArg().setValue("-sourcepath");
        scalac.createArg().setPath(sourcePath);

        // Set up output directory
        scalac.createArg().setValue("-d");
        scalac.createArg().setFile(destDir);

        String jvmArgs = properties().getString("jvmArgs", null);

        if (jvmArgs != null) {
            scalac.createJvmarg().setLine(jvmArgs);
        }

        String compilerArgs = properties().getString("compilerArgs", null);

        if (compilerArgs != null) {
            scalac.createArg().setLine(compilerArgs);
        }

        // Add the files to compile
        List files = compileJava ? sourceFiles.getAll() : sourceFiles.getScala();
        log().verbose("Files to be compiled:");

        for (Iterator i = files.iterator(); i.hasNext();) {
            File file = (File)i.next();
            scalac.createArg().setFile(file);

            if (log().isVerboseEnabled()) {
                log().verbose("\t" + file.getPath());
            }
        }

        scalac.setFailonerror(true);
        scalac.setClassname(useFsc ? FSC : "scala.tools.nsc.Main");

        if (useFsc && waitForFsc) {
            CompileServerHelper helper = new CompileServerHelper(this);
            helper.waitForCompileServer();
        }

        scalac.perform();
    }

    public Java createScalaCommand(String taskName) {
        Java command = (Java)utils().init(new Java(), taskName);

        // Ant's bootstrap can't handle the append /a: notation, so set up manually
        command.createJvmarg().setValue("-Xbootclasspath/a:" + getState().get(BOOTSTRAP_CLASS_PATH));
        command.setMaxmemory(properties().getString("maxMemory", null));
        command.setFork(true);
        command.setClasspath((Path)getState().get(COMPILER_CLASS_PATH));

        return command;
    }

    /**
     * Returns the source path, including Java and Scala sources. Allows for Java files
     * to be stored in a separated source path root or mixed in the same source root.
     */
    private Path getSourcePath() {
        Path src = new Path(getProject());
        File srcDir = properties().getFile("srcDir", null);

        if ((srcDir != null) && srcDir.exists()) {
            src.append(new Path(getProject(), srcDir.getPath()));
        }

        File javaSrcDir = properties().getFile("javac.srcDir", null);

        if ((javaSrcDir != null) && javaSrcDir.exists() && !javaSrcDir.equals(srcDir)) {
            src.append(new Path(getProject(), javaSrcDir.getPath()));
        }

        return src;
    }

    public Files getSourceFiles(Path sourcepath, boolean force) {
        Files allFiles = new Files(new ArrayList(), new ArrayList());

        for (Iterator i = sourcepath.iterator(); i.hasNext();) {
            File file = ((FileResource)i.next()).getFile();
            Files files = getSourceFiles(file, force);
            allFiles.getJava().addAll(files.getJava());
            allFiles.getScala().addAll(files.getScala());
        }

        return allFiles;
    }

    public Files getSourceFiles(File source, boolean force) {
        List scalaFiles = new ArrayList();
        List javaFiles = new ArrayList();
        long timestamp = getTimestampFile().lastModified();

        for (Iterator i = utils().toFileSet(source).iterator(); i.hasNext();) {
            File file = ((FileResource)i.next()).getFile();

            if (force || (file.lastModified() > timestamp)) {
                if (file.getName().endsWith(".scala")) {
                    scalaFiles.add(file);
                } else if (file.getName().endsWith(".java")) {
                    javaFiles.add(file);
                }
            }
        }

        return new Files(javaFiles, scalaFiles);
    }

    public void fscShutdown() {
        Java fsc = createScalaCommand("fsc");
        fsc.setClassname(FSC);
        fsc.createArg().setValue("-shutdown");
        fsc.setInputString("dummy");
        fsc.execute();
    }

    public void docsCollect() {
        getScalaDocsHelper().collect();
    }

    public void docsPropogate() {
        getScalaDocsHelper().propogate();
    }

    public void docsReport() {
        getScalaDocsHelper().report();
    }

    public void docsAggregateReport() {
        getScalaDocsHelper().aggregateReport();
    }

    public void runTemplate() {
        Java java = createScalaCommand("run");
        java.setClassname(properties().getString("mainClass"));

        Path classpath = new Path(getProject(), getLifeCycle().getComplieOutput().getAbsolutePath());
        classpath.append(new Path(getProject(), getLifeCycle().getTestComplieOutput().getAbsolutePath()));
        classpath.append(getResources().getPathGroupAsPath("run"));
        java.setClasspath(classpath);

        for (Iterator i = properties().getList("arg", false, String.class, false).iterator(); i.hasNext();) {
            String arg = (String)i.next();
            java.createArg().setValue(arg);
        }

        String args = properties().getString("args", null);

        if (args != null) {
            java.createArg().setLine(args);
        }

        String jvmArgs = properties().getString("jvmArgs", null);

        if (jvmArgs != null) {
            java.createJvmarg().setLine(jvmArgs);
        }

        java.perform();
    }

    private ScalaDocsHelper getScalaDocsHelper() {
        return new ScalaDocsHelper(this, getResources(), properties(), log());
    }

    //~ Inner Classes --------------------------------------------------------------------------------------------------

    private static class Files {
        private List java;
        private List scala;

        private Files(List java, List scala) {
            this.java = java;
            this.scala = scala;
        }

        public List getJava() {
            return java;
        }

        public List getScala() {
            return scala;
        }

        public List getAll() {
            List list = new ArrayList(java);
            list.addAll(scala);

            return list;
        }

        public String toString(boolean includeJava) {
            String s = toString(scala, "scala");
            String j = includeJava ? toString(java, "java") : "";

            return s + ((!s.equals("") && !j.equals("")) ? " and " : "") + j;
        }

        private String toString(List list, String type) {
            return (list.size() == 0) ? "" : (list.size() + " " + type + ((list.size() == 1) ? " file" : " files"));
        }
    }
}
